#!/usr/bin/env python
#-*- coding: utf-8 -*-
#
# BSD License
# Copyright (c) 2011, Wang Qiang
# All rights reserved.

"""
HMTLSelectorAPI

Process HTML file and XML file. 

Function list:

Three basic parser:
elements = parseByElement(html, elementName)
elements = parseByTagProperties(html, tagName, **properties)
elements = parseByProperties(html, **properties)

Advanced Selector:
elements = selectByClass(html, classname)
elements = selectById(html, id)

JQuery-like Selector:
elements = JQSelect(html, selectStr)

selectStr specification
'tag.class#id[name="value"]'
[name|="value"]
[name*="value"]
[name~="value"]
[name!="value"]
[name^="value"]
[name$="value"]
"""
import re
from BaseHTMLProcessor import BaseHTMLProcessor

def JQSelect(html, selectStr):
    """
    elements = JQSelect(html, selectStr)
    Implement JQuery-like selecting function
    @param html: input html/xml
    @param selectStr: JQuery-like select string.
    @return: elements, list of matched elements  
    """
    if selectStr == '*':
        # select entire body. 
        return parseByElement(html, "body")  
    else:
        return processJQSelector(html, selectStr)


def processJQSelector(html, selectStr):
    """
    elements = processJQSelector(html, selectStr)
    Implement JQuery-like selecting function without '*'
    @param html: input html/xml
    @param selectStr: JQuery-like select string.
    @return: elements, list of matched elements  
    """
    selectors = [s.strip() for s in selectStr.split(',')]
    elements = []
    for selector in selectors:
        elements += processSingleSelector(html, selector) and \
        processSingleSelector(html, selector) or []
    return elements

def processSingleSelector(html, selectStr):
    """
    elements = processSingleSelector(html, selectStr)
    Implement JQuery-like selecting for a single selector
    @param html: input html/xml
    @param selectStr: JQuery-like select string.
    @return: elements, list of matched elements  
    """
    if ' > ' in selectStr:
        # child match
        selectors = [s.strip() for s in selectStr.split('>')]
        htmls = processSimpleSelector(html, "body")
        for selector in selectors:
            for chtml in htmls:
                htmls = processSimpleSelector(chtml, selector)
        return htmls
    elif ' + ' in selectStr:
        # next to match
        pass
    elif ' ~ ' in selectStr:
        # sibling match
        pass
    else:
        # Simple Match
        return processSimpleSelector(html, selectStr)
    
def processSimpleSelector(html, selectStr):
    """
    elements = processSimpleSelector(html, selectStr)
    Implement JQuery-like selecting for a single simple selector
    @param html: input html/xml
    @param selectStr: JQuery-like select string.
    @return: elements, list of matched elements  
    """
    selstrs = selectStr.split('[', 1)
    tagBasic = selstrs[0]
    tagAttrs = []
    tag = None
    id = None
    className = None
    if len(selstrs) > 1:
        tagAttrs =  selstrs[1][:-1].split('][')
    basicRe = r'(?P<tag>[^\.#]+)?(?P<class1>\.[^\.#]+)?(?P<id>#[^\.#]+)?(?P<class2>\.[^\.#]+)?'
    m = re.match(basicRe, tagBasic)
    if m:
        tag = m.group('tag') and m.group('tag') or tag
        id = m.group('id') and m.group('id')[1:] or id
        className = m.group('class1') and m.group('class1')[1:] or className
        className = m.group('class2') and m.group('class2')[1:] or className
    properties = {}
    if id:
        properties['id'] = id
    if className:
        properties['class'] = className
    properties.update(processJQProperties(tagAttrs))
    bselector = BasicSelector()
    if tag and properties:
        return bselector.parseByTagProperties(html, tag, **properties)
    elif tag:
        return bselector.parseByElement(html, tag)
    elif properties:
        return bselector.parseByProperties(html, **properties)
    else:
        return bselector.parseByElement(html, "body")
    
def processJQProperties(properties):
    """
    resDict = processJQProperties(properites)
    Analysis JQuery properties pair.
    @param properties: properties pair 
    @return: resDict, sorted properties pair
    """
    resDict = {}
    for pair in properties:
        resDict.update(processJQSingleProperty(pair))
    return resDict

def processJQSingleProperty(pair):
    """
    resDict = processJQSingleProperty(pair)
    Analysis one JQuery property pair.
    @param pair: one property pair 
    @return: resDict, sorted property pair
    """
    if '=' in pair:
        rePattern = r'(?P<name>[^!\|\$\*\^~=]+)(?P<operator>[^"]+)"(?P<value>[^"]+)"'
        m = re.match(rePattern, pair)
        if m:
            name = m.group('name')
            operator = m.group('operator')
            value = m.group('value')
            if operator == '=':
                return {name : '^' + value + '$'}
            if operator == '|=':
                return {name : '^' + value + '-?.*'}
            if operator == '*=':
                return {name : '.*' + value + '.*'}
            if operator == '~=':
                return {name : r'\b' + value + r'\b'}
            if operator == '$=':
                return {name : '.*' + value + '$'}
            if operator == '^=':
                return {name : '^' + value + '.*'}
            if operator == '!=':
                return {name : r'^(?:(?!'+ value + r').)*$'}
        else:
            return {}
    else:
        return {pair : '.*'}   

def selectByClass(html, classname):
    """
    elements = selectByClass(html, classname)
    select HTML/XML elements by class name.
    @param classname: class name filter
    @param html: html source 
    @return: elements, list to matched elements 
    """
    bselector = BasicSelector()
    return bselector.parseByProperties(html, CLASS=classname)

def selectById(html, id):
    """
    elements = selectById(html, id)
    select HTML/XML elements by id.
    @param id: id filter
    @param html: html source 
    @return: elements, list to matched elements 
    """
    bselector = BasicSelector()
    return bselector.parseByProperties(html, ID=id)


def matchProperties(attrs, properties):
    """
    Compare the attrs and properties.
    @return: res, property is matched.
    """
    result = True
    for k, v in properties.items():
        stepRes = False
        attrkeys = [ka for ka, va in attrs]
        if '^(?:(?!' in v and k not in attrkeys:
            continue
        for ka, va in attrs:
            if k.lower() == ka.lower() and re.match(v, va):
                stepRes = True
        if not stepRes:
            result = False
            break
    return result


class BasicSelector:
    """
    HTML/XML selector
    """
    def __init__(self):
        """
        Constructor
        """
        pass
    
    def parseByElement(self, html, elementName):
        """
        elements = parseByElement(html, elementName)
        parse HTML/XML elements by element name.
        @param elementName: element name filter
        @param html: html source 
        @return: elements, list to matched elements 
        """
        elements = []
        class MyHTMLParser(BaseHTMLProcessor):
            """
            Template parser
            """
            def handle_starttag(self, tag, attrs):
                """called for each start tag"""
                if tag.lower() == elementName:
                    self.counter += 1
                if self.counter > 0:
                    strattrs = "".join([' %s="%s"' % (key, value) for \
                                        key, value in attrs])
                    self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
        
            def handle_endtag(self, tag):
                """called for each end tag"""
                if self.counter > 0:
                    self.pieces.append("</%(tag)s>" % locals())
                    if tag.lower() == elementName:
                        self.counter -= 1
                        if self.counter == 0:
                            elements.append(self.output())
                            self.resetPiece()
                            
            def handle_startendtag(self, tag, attrs):
                """called for start end tag"""
                if tag.lower() == elementName:
                    self.counter += 1
                if self.counter > 0:
                    strattrs = "".join([' %s="%s"' % (key, value) for \
                                        key, value in attrs])
                    self.pieces.append("<%(tag)s%(strattrs)s />" % locals())
                    if tag.lower() == elementName:
                        self.counter -= 1
                        if self.counter == 0:
                            elements.append(self.output())
                            self.resetPiece()
                    
        parser = MyHTMLParser()
        parser.feed(html)
        return elements
    
    def parseByTagProperties(self, html, tagName, **properties):
        """
        elements = parseByTagProperties(html, tagName, **properties)
        parse HTML/XML elements by property pair and tag name.
        @param properties: property pair
        @param tagName: tag name 
        @param html: html source 
        @return: elements, list to matched elements 
        """
        elements = []
        elementName = tagName.lower()
        class MyHTMLParser(BaseHTMLProcessor):
            """
            Template parser
            """
            def handle_starttag(self, tag, attrs):
                """called for each start tag"""
                if tag.lower() == elementName:
                    if self.counter == 0:
                        if matchProperties(attrs, properties):
                            self.counter += 1
                    elif self.counter > 0:
                        self.counter += 1
                if self.counter > 0:
                    strattrs = "".join([' %s="%s"' % (key, value) for \
                                        key, value in attrs])
                    self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
        
            def handle_endtag(self, tag):
                """called for each end tag"""
                if self.counter > 0:
                    self.pieces.append("</%(tag)s>" % locals())
                    if tag.lower() == elementName:
                        self.counter -= 1
                        if self.counter == 0:
                            elements.append(self.output())
                            self.resetPiece()
                            
            def handle_startendtag(self, tag, attrs):
                """called for start end tag"""
                if tag.lower() == elementName:
                    if self.counter == 0:
                        if matchProperties(attrs, properties):
                            self.counter += 1
                    elif self.counter > 0:
                        self.counter += 1
                if self.counter > 0:
                    strattrs = "".join([' %s="%s"' % (key, value) for \
                                        key, value in attrs])
                    self.pieces.append("<%(tag)s%(strattrs)s />" % locals())
                    if tag.lower() == elementName:
                        self.counter -= 1
                        if self.counter == 0:
                            elements.append(self.output())
                            self.resetPiece()
                    
                    
        parser = MyHTMLParser()
        parser.feed(html)
        return elements
    
    
    def parseByProperties(self, html, **properties):
        """
        elements = parseByProperties(html, **properties)
        parse HTML/XML elements by property pair.
        @param properties: property pair, 
        @param html: html source 
        @return: elements, list to matched elements 
        """
        elements = []
        class MyHTMLParser(BaseHTMLProcessor):
            """
            Template parser
            """
            elementName = None
            def handle_starttag(self, tag, attrs):
                """called for each start tag"""
                if self.counter == 0:
                    if matchProperties(attrs, properties):
                        self.counter += 1
                        self.elementName = tag
                elif tag == self.elementName:
                        self.counter += 1
                if self.counter > 0:
                    strattrs = "".join([' %s="%s"' % (key, value) for \
                                        key, value in attrs])
                    self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
        
            def handle_endtag(self, tag):
                """called for each end tag"""
                if self.counter > 0:
                    self.pieces.append("</%(tag)s>" % locals())
                    if tag == self.elementName:
                        self.counter -= 1
                        if self.counter == 0:
                            elements.append(self.output())
                            self.resetPiece()
                            
            def handle_startendtag(self, tag, attrs):
                """called for start end tag"""
                if self.counter == 0:
                    if matchProperties(attrs, properties):
                        self.counter += 1
                        self.elementName = tag
                elif tag == self.elementName:
                        self.counter += 1
                if self.counter > 0:
                    strattrs = "".join([' %s="%s"' % (key, value) for \
                                        key, value in attrs])
                    self.pieces.append("<%(tag)s%(strattrs)s />" % locals())
                    if tag == self.elementName:
                        self.counter -= 1
                        if self.counter == 0:
                            elements.append(self.output())
                            self.resetPiece()
                    
        parser = MyHTMLParser()
        parser.feed(html)
        return elements
    
    